import { Codec, CodecOption, Constructor, Decoder, defaultCharSingleByte, defaultCharUnicode, Encoder, Encoding, Options } from '../models.js';

import { tableInit } from './tables/index.js';

const UNASSIGNED = -1;
const GB18030_CODE = -2;
const SEQ_START = -10;
const NODE_START = -1000;
const UNASSIGNED_NODE = new Array(0x100);
const DEF_CHAR = -1;

for (let i = 0; i < 0x100; i++) {
  UNASSIGNED_NODE[i] = UNASSIGNED;
}

class DBCSCodec implements Codec {
  encodingName: string;
  defaultCharUnicode: string;
  defCharSB: any;
  decodeTables: any[];
  decodeTableSeq: any[];
  encodeTable: any[];
  encodeTableSeq: any[];
  decoder: Constructor<Decoder> = DBCSDecoder;
  encoder: Constructor<Encoder> = DBCSEncoder;

  constructor(codecOptions: CodecOption) {
    this.encodingName = codecOptions.encodingName!;
    if (!codecOptions) throw new Error('DBCS codec is called without the data.');
    if (!codecOptions.table) throw new Error(`Encoding '${this.encodingName}' has no data.`);

    const mappingTable = codecOptions.table();
    this.decodeTables = [];
    this.decodeTables[0] = UNASSIGNED_NODE.slice(0);

    this.decodeTableSeq = [];

    for (let i = 0; i < mappingTable.length; i++) {
      this.addDecodeChunk(mappingTable[i]);
    }

    this.defaultCharUnicode = defaultCharUnicode;

    // Encode tables: Unicode -> DBCS.
    this.encodeTable = [];

    this.encodeTableSeq = [];

    // Some chars can be decoded, but need not be encoded.
    const skipEncodeChars = {};

    // Use decode trie to recursively fill out encode tables.
    this.fillEncodeTable(0, 0, skipEncodeChars);

    this.defCharSB = this.encodeTable[0][defaultCharSingleByte.charCodeAt(0)];
    if (this.defCharSB === UNASSIGNED) this.defCharSB = this.encodeTable[0]['?'];
    if (this.defCharSB === UNASSIGNED) this.defCharSB = '?'.charCodeAt(0);
  }

  private addDecodeChunk(chunk: any[]) {
    // First element of chunk is the hex mbcs code where we start.
    let curAddr = parseInt(chunk[0], 16);

    // Choose the decoding node where we'll write our chars.
    const writeTable = this.getDecodeTrieNode(curAddr);
    curAddr = curAddr & 0xff;

    // Write all other elements of the chunk to the table.
    for (let k = 1; k < chunk.length; k++) {
      const part = chunk[k];
      if (typeof part === 'string') {
        for (let l = 0; l < part.length; ) {
          const code = part.charCodeAt(l++);
          if (0xd800 <= code && code < 0xdc00) {
            const codeTrail = part.charCodeAt(l++);
            if (0xdc00 <= codeTrail && codeTrail < 0xe000) writeTable[curAddr++] = 0x10000 + (code - 0xd800) * 0x400 + (codeTrail - 0xdc00);
            else throw new Error(`Incorrect surrogate pair in ${this.encodingName} at chunk ${chunk[0]}`);
          } else if (0x0ff0 < code && code <= 0x0fff) {
            const len = 0xfff - code + 2;
            const seq = [];
            for (let m = 0; m < len; m++) seq.push(part.charCodeAt(l++));

            writeTable[curAddr++] = SEQ_START - this.decodeTableSeq.length;
            this.decodeTableSeq.push(seq);
          } else writeTable[curAddr++] = code;
        }
      } else if (typeof part === 'number') {
        let charCode = writeTable[curAddr - 1] + 1;
        for (let l = 0; l < part; l++) writeTable[curAddr++] = charCode++;
      } else throw new Error(`Incorrect type '${typeof part}' given in ${this.encodingName} at chunk ${chunk[0]}`);
    }
    if (curAddr > 0xff) throw new Error(`Incorrect chunk in ${this.encodingName} at addr ${chunk[0]}: too long${curAddr}`);
  }

  private getDecodeTrieNode(addr: number) {
    const bytes = [];
    for (; addr > 0; addr >>>= 8) bytes.push(addr & 0xff);
    if (bytes.length === 0) bytes.push(0);

    let node = this.decodeTables[0];
    for (let i = bytes.length - 1; i > 0; i--) {
      const val = node[bytes[i]];

      if (val === UNASSIGNED) {
        node[bytes[i]] = NODE_START - this.decodeTables.length;
        this.decodeTables.push((node = UNASSIGNED_NODE.slice(0)));
      } else if (val <= NODE_START) {
        node = this.decodeTables[NODE_START - val];
      } else throw new Error(`Overwrite byte in ${this.encodingName}, addr: ${addr.toString(16)}`);
    }
    return node;
  }

  private fillEncodeTable(nodeIdx: number, prefix: number, skipEncodeChars: { [key: string | number]: boolean }) {
    const node = this.decodeTables[nodeIdx];
    let hasValues = false;
    const subNodeEmpty: { [key: string | number]: boolean } = {};
    for (let i = 0; i < 0x100; i++) {
      const uCode = node[i];
      const mbCode = prefix + i;
      if (skipEncodeChars[mbCode]) continue;

      if (uCode >= 0) {
        this.setEncodeChar(uCode, mbCode);
        hasValues = true;
      } else if (uCode <= NODE_START) {
        const subNodeIdx = NODE_START - uCode;
        if (!subNodeEmpty[subNodeIdx]) {
          const newPrefix = (mbCode << 8) >>> 0;
          if (this.fillEncodeTable(subNodeIdx, newPrefix, skipEncodeChars)) hasValues = true;
          else subNodeEmpty[subNodeIdx] = true;
        }
      } else if (uCode <= SEQ_START) {
        this.setEncodeSequence(this.decodeTableSeq[SEQ_START - uCode], mbCode);
        hasValues = true;
      }
    }
    return hasValues;
  }

  private setEncodeChar(uCode: number, dbcsCode: number) {
    const bucket = this.getEncodeBucket(uCode);
    const low = uCode & 0xff;
    if (bucket[low] <= SEQ_START) this.encodeTableSeq[SEQ_START - bucket[low]][DEF_CHAR] = dbcsCode;
    else if (bucket[low] === UNASSIGNED) bucket[low] = dbcsCode;
  }

  private setEncodeSequence(seq: number[], dbcsCode: number) {
    let uCode = seq[0];
    const bucket = this.getEncodeBucket(uCode);
    const low = uCode & 0xff;

    let node: any = {};
    if (bucket[low] <= SEQ_START) {
      node = this.encodeTableSeq[SEQ_START - bucket[low]];
    } else {
      if (bucket[low] !== UNASSIGNED) {
        node[DEF_CHAR] = bucket[low];
      }
      bucket[low] = SEQ_START - this.encodeTableSeq.length;
      this.encodeTableSeq.push(node);
    }

    // Traverse the character tree, allocating new nodes as needed.
    for (let j = 1; j < seq.length - 1; j++) {
      const oldVal: any = node[uCode];
      if (typeof oldVal === 'object') node = oldVal;
      else {
        node = node[uCode] = {};
        if (oldVal !== undefined) node[DEF_CHAR] = oldVal;
      }
    }

    // Set the leaf to given dbcsCode.
    uCode = seq[seq.length - 1];
    node[uCode] = dbcsCode;
  }

  private getEncodeBucket(uCode: number) {
    const high = uCode >> 8;
    if (this.encodeTable[high] === undefined) this.encodeTable[high] = UNASSIGNED_NODE.slice(0);
    return this.encodeTable[high];
  }
}

/**
 * DBCSDecoder 解码器
 */
class DBCSDecoder implements Decoder {
  private nodeIdx: number;
  private prevBytes: any[];
  private decodeTables: any[];
  private decodeTableSeq: any[];
  private defaultCharUnicode: string;

  constructor(options: Options, codec: Codec) {
    // Decoder state
    this.nodeIdx = 0;
    this.prevBytes = [];

    // Static data
    this.decodeTables = codec.decodeTables!;
    this.decodeTableSeq = codec.decodeTableSeq!;
    this.defaultCharUnicode = codec.defaultCharUnicode!;
  }

  write(buf: Buffer | Uint8Array): string {
    const newBuf = Buffer.alloc(buf.length * 2);
    let nodeIdx = this.nodeIdx;
    const prevBytes = this.prevBytes;
    const prevOffset = this.prevBytes.length;
    let seqStart = -this.prevBytes.length;
    let uCode;
    let j = 0;
    for (let i = 0; i < buf.length; i++) {
      const curByte = i >= 0 ? buf[i] : prevBytes[i + prevOffset];

      // Lookup in current trie node.
      uCode = this.decodeTables[nodeIdx][curByte];

      if (uCode >= 0) {
        // Normal character, just use it.
      } else if (uCode === UNASSIGNED) {
        uCode = this.defaultCharUnicode.charCodeAt(0);
        i = seqStart;
      } else if (uCode <= NODE_START) {
        nodeIdx = NODE_START - uCode;
        continue;
      } else if (uCode <= SEQ_START) {
        const seq = this.decodeTableSeq[SEQ_START - uCode];
        for (let k = 0; k < seq.length - 1; k++) {
          uCode = seq[k];
          newBuf[j++] = uCode & 0xff;
          newBuf[j++] = uCode >> 8;
        }
        uCode = seq[seq.length - 1];
      } else throw new Error(`iconv error: invalid decoding table value ${uCode} at ${nodeIdx}/${curByte}`);

      // Write the character to buffer, handling higher planes using surrogate pair.
      if (uCode >= 0x10000) {
        uCode -= 0x10000;
        const uCodeLead = 0xd800 | (uCode >> 10);
        newBuf[j++] = uCodeLead & 0xff;
        newBuf[j++] = uCodeLead >> 8;

        uCode = 0xdc00 | (uCode & 0x3ff);
      }
      newBuf[j++] = uCode & 0xff;
      newBuf[j++] = uCode >> 8;

      // Reset trie node.
      nodeIdx = 0;
      seqStart = i + 1;
    }

    this.nodeIdx = nodeIdx;
    this.prevBytes = seqStart >= 0 ? Array.prototype.slice.call(buf, seqStart) : prevBytes.slice(seqStart + prevOffset).concat(Array.prototype.slice.call(buf));

    return newBuf.slice(0, j).toString('ucs2');
  }

  end(): string | undefined {
    let ret = '';

    while (this.prevBytes.length > 0) {
      ret += this.defaultCharUnicode;
      const bytesArr = this.prevBytes.slice(1);
      this.nodeIdx = 0;
      this.prevBytes = [];
      if (bytesArr.length > 0) ret += this.write(Buffer.from(bytesArr));
    }

    this.prevBytes = [];
    this.nodeIdx = 0;
    return ret;
  }

  private findIdx(table: number[], val: number) {
    if (table[0] > val) return -1;

    let l = 0,
      r = table.length;
    while (l < r - 1) {
      const mid = l + ((r - l + 1) >> 1);
      if (table[mid] <= val) l = mid;
      else r = mid;
    }
    return l;
  }
}

/**
 * DBCSEncoder 编码器
 */
class DBCSEncoder implements Encoder {
  private leadSurrogate: number;
  private seqObj: any;
  private encodeTable: any[];
  private encodeTableSeq: any[];
  private defaultCharSingleByte: any;

  constructor(options: Options, codec: Codec) {
    // Encoder state
    this.leadSurrogate = -1;
    this.seqObj = undefined;

    // Static data
    this.encodeTable = codec.encodeTable!;
    this.encodeTableSeq = codec.encodeTableSeq!;
    this.defaultCharSingleByte = codec.defCharSB!;
  }

  write(str: string): Buffer {
    const args = {
      str: str,
      newBuf: Buffer.alloc(str.length * 3),
      leadSurrogate: this.leadSurrogate,
      seqObj: this.seqObj,
      nextChar: -1,
      i: 0,
      j: 0,
      uCode: undefined,
    };

    while (!(args.nextChar === -1 && args.i === str.length)) {
      this.getNextCharCode(args);
      if (args.seqObj !== undefined && args.uCode !== UNASSIGNED) {
        this.handleSeqObj(args);
      }

      if (args.uCode! >= 0) {
        const dbcsCode = this.getDbcsCode(args);
        this.writeDbcs(args, dbcsCode);
      }
    }

    this.seqObj = args.seqObj;
    this.leadSurrogate = args.leadSurrogate;

    return args.newBuf.slice(0, args.j);
  }

  private getNextCharCode(args: any): void {
    if (args.nextChar === -1) {
      args.uCode = args.str.charCodeAt(args.i++);
    } else {
      args.uCode = args.nextChar;
      args.nextChar = -1;
    }

    if (0xd800 <= args.uCode && args.uCode < 0xe000) {
      if (args.uCode < 0xdc00) {
        args.leadSurrogate = args.uCode;
      } else if (args.leadSurrogate !== -1) {
        args.uCode = 0x10000 + (args.leadSurrogate - 0xd800) * 0x400 + (args.uCode - 0xdc00);
        args.leadSurrogate = -1;
      } else {
        args.uCode = UNASSIGNED;
      }
    } else if (args.leadSurrogate !== -1) {
      args.leadSurrogate = -1;
      args.nextChar = args.uCode;
      args.uCode = UNASSIGNED;
    }
  }

  private handleSeqObj(args: any): void {
    let resCode = args.seqObj[args.uCode];

    if (typeof resCode === 'object') {
      args.seqObj = resCode;
    } else if (typeof resCode === 'number') {
      args.seqObj = undefined;
    } else if (resCode === undefined) {
      resCode = args.seqObj[DEF_CHAR];

      if (resCode !== undefined) {
        args.seqObj = undefined;
        args.nextChar = args.uCode;
        args.uCode = resCode;
      }
    }
  }

  private getDbcsCode(args: any): number {
    let dbcsCode = UNASSIGNED;
    const subtable = this.encodeTable[args.uCode >> 8];
    if (subtable !== undefined) dbcsCode = subtable[args.uCode & 0xff];

    if (dbcsCode <= SEQ_START) {
      args.seqObj = this.encodeTableSeq[SEQ_START - dbcsCode];
      return UNASSIGNED;
    }

    if (dbcsCode === UNASSIGNED) dbcsCode = this.defaultCharSingleByte;

    return dbcsCode;
  }

  private writeDbcs(args: any, dbcsCode: number): void {
    if (dbcsCode < 0x100) {
      args.newBuf[args.j++] = dbcsCode;
    } else if (dbcsCode < 0x10000) {
      args.newBuf[args.j++] = dbcsCode >> 8;
      args.newBuf[args.j++] = dbcsCode & 0xff;
    } else if (dbcsCode < 0x1000000) {
      args.newBuf[args.j++] = dbcsCode >> 16;
      args.newBuf[args.j++] = (dbcsCode >> 8) & 0xff;
      args.newBuf[args.j++] = dbcsCode & 0xff;
    } else {
      args.newBuf[args.j++] = dbcsCode >>> 24;
      args.newBuf[args.j++] = (dbcsCode >>> 16) & 0xff;
      args.newBuf[args.j++] = (dbcsCode >>> 8) & 0xff;
      args.newBuf[args.j++] = dbcsCode & 0xff;
    }
  }

  end(): Buffer | undefined {
    if (this.leadSurrogate === -1 && this.seqObj === undefined) return;

    const newBuf = Buffer.alloc(10);
    let j = 0;

    if (this.seqObj) {
      const dbcsCode = this.seqObj[DEF_CHAR];
      if (dbcsCode !== undefined) {
        if (dbcsCode < 0x100) {
          newBuf[j++] = dbcsCode;
        } else {
          newBuf[j++] = dbcsCode >> 8;
          newBuf[j++] = dbcsCode & 0xff;
        }
      } else {
        // See todo above.
      }
      this.seqObj = undefined;
    }

    if (this.leadSurrogate !== -1) {
      newBuf[j++] = this.defaultCharSingleByte;
      this.leadSurrogate = -1;
    }

    return newBuf.slice(0, j);
  }
}

export const DBCSEncoding: Encoding = {
  // Encodings
  gbk: {
    type: '_dbcs',
    table: function () {
      return tableInit('cp936.json').concat(tableInit('gbk-added.json'));
    },
  },

  // Codec.
  _dbcs: DBCSCodec,
};
